﻿using System;
using System.Collections.Generic;
using System.Linq;

namespace Array3DExtensions
{
    /// <summary>
    /// Adds functionality to 3D T arrays
    /// Stanislav Radkov®  2012 :D 
    /// 
    /// Assumes that all arrays are created in the followin manner:
    ///         Int32[,,] array = new Int32[W, H, D];
    /// </summary>
    public static class Array3DExtensions
    {
        /// <summary>
        /// Determines whether a cell has a valid index which is in the boundaries of the array.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="cellIndex">Index of the cell.</param>
        /// <returns>
        ///   <c>true</c> if the index is valid otherwise<c>false</c>.
        /// </returns>
        public static bool IsInBoundary<T>(this T[,,] array, Index3D cellIndex)
        {
            bool isWidthValid = cellIndex.W >= 0 && cellIndex.W < array.GetLength(0);
            bool isHeightValid = cellIndex.H >= 0 && cellIndex.H < array.GetLength(1);
            bool isDepttValid = cellIndex.D >= 0 && cellIndex.D < array.GetLength(2);

            return isWidthValid && isHeightValid && isDepttValid;
        }

        /// <summary>
        /// Initializes the array with the given value.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="value">The value.</param>
        public static void Init<T>(this T[,,] array, T value)
        {            
            for (int w = 0; w < array.GetLength(0); w++)
            {
                for (int h = 0; h < array.GetLength(1); h++)
                {
                    for (int d = 0; d < array.GetLength(2); d++)
                    {
                        array[w, h, d] = value;
                    }
                }
            }
        }

        /// <summary>
        /// Gets the value with the given index
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="cellIndex">Index of the cell.</param>
        /// <returns>The value in the given cell</returns>
        /// <exception cref="System.IndexOutOfRangeException"></exception>
        public static T GetValue<T>(this T[,,] array, Index3D cellIndex)
        {
            if (!array.IsInBoundary(cellIndex))
            {
                string message = string.Format("The given index is not in the boundaries of the array! Given index:{0} , Max index in the array:{1}", cellIndex, new Index3D(array.GetLength(0), array.GetLength(1), array.GetLength(2)));
                throw new IndexOutOfRangeException(message);
            }

            T value = array[cellIndex.W, cellIndex.H, cellIndex.D];
            return value;
        }

        /// <summary>
        /// Sets the value
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="cell">The cell.</param>
        /// <exception cref="System.IndexOutOfRangeException"></exception>
        public static void SetValue<T>(this T[,,] array, Cell3D<T> cell) where T : IComparable<T>
        {
            if (!array.IsInBoundary(cell as Index3D))
            {
                string message = string.Format("The given index is not in the boundaries of the array! Given index:{0} , Max index in the array:{1}", cell,new Index3D(array.GetLength(0), array.GetLength(1), array.GetLength(2)));
                throw new IndexOutOfRangeException(message);
            }

            array[cell.W, cell.H, cell.D] = cell.Value;
        }

        /// <summary>
        /// Sets the value with the given index
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="index">The index of the cell.</param>
        /// <param name="value">The value.</param>
        /// <exception cref="System.IndexOutOfRangeException"></exception>
        public static void SetValue<T>(this T[,,] array, Index3D index, T value)
        {
            if (!array.IsInBoundary(index))
            {
                string message = string.Format("The given index is not in the boundaries of the array! Given index:{0} , Max index in the array:{1}", index, new Index3D(array.GetLength(0), array.GetLength(1), array.GetLength(2)));
                throw new IndexOutOfRangeException(message);
            }

            array[index.W, index.H, index.D] = value;
        }

        /// <summary>
        /// Searches for the value in  the given directions.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="value">The value.</param>
        /// <param name="startIndex">The start index.</param>
        /// <param name="direction">The direction.</param>
        /// <returns><c>True</c> if the value is found</returns>
        /// <exception cref="System.IndexOutOfRangeException"></exception>
        public static bool SearchInDirections<T>(this T[,,] array, T value, Index3D startIndex, Index3D direction) where T : IComparable<T>
        {
            if (!array.IsInBoundary(startIndex))
            {
                string message = string.Format("The given index is not in the boundaries of the array! Given index:{0} , Max index in the array:{1}", startIndex, new Index3D(array.GetLength(0), array.GetLength(1), array.GetLength(2)));
                throw new IndexOutOfRangeException(message);
            }

            bool found = false;
            Index3D currentPosition = startIndex;

            while (array.IsInBoundary(currentPosition))
            {
                if (array.GetValue(currentPosition).CompareTo(value) == 0)
                {
                    found = true;
                    break;
                }
                else
                {
                    currentPosition += direction;
                }
            }

            return found;
        }

        /// <summary>
        /// Returns the indexes of the cells which contain the given value
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="value">The value.</param>
        /// <returns>Indexes of the cells which contain the value</returns>
        public static List<Index3D> IndexesOf<T>(this T[,,] array, T value) where T : IComparable
        {
            List<Index3D> cellsContainingValue = new List<Index3D>();

            for (int w = 0; w < array.GetLength(0); w++)
            {
                for (int h = 0; h < array.GetLength(1); h++)
                {
                    for (int d = 0; d < array.GetLength(2); d++)
                    {
                        if (array[w, h, d].CompareTo(value) == 0)
                        {
                            cellsContainingValue.Add(new Index3D(w, h, d));
                        }
                    }
                }
            }

            return cellsContainingValue;
        }

        /// <summary>
        /// Searches for the max value in the given direction while in the boundaries of the array
        /// </summary>
        /// <param name="startIndex">The position to start the search from</param>
        /// <param name="direction"> The direction to search in </param>
        /// <returns>Max value</returns>
        /// <exception cref="System.IndexOutOfRangeException"></exception>
        public static T SearchForMaxValueInDirection<T>(this T[,,] array, Index3D startIndex, Index3D direction) where T : IComparable
        {
            if (!array.IsInBoundary(startIndex))
            {
                string message = string.Format("The given index is not in the boundaries of the array! Given index:{0} , Max index in the array:{1}", startIndex, new Index3D(array.GetLength(0), array.GetLength(1), array.GetLength(2)));
                throw new IndexOutOfRangeException(message);
            }

            T maxValue = array[0, 0, 0];
            Index3D currentPosition = startIndex;

            while (array.IsInBoundary(currentPosition))
            {
                if (array.GetValue(currentPosition).CompareTo(maxValue) > 0)
                {
                    maxValue = array.GetValue(currentPosition);
                }
                else
                {
                    currentPosition += direction;
                }
            }

            return maxValue;
        }

        /// <summary>
        /// Collects all the items from a start index in a given direction
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="startIndex">The start index.</param>
        /// <param name="direction">The direction.</param>
        /// <exception cref="System.IndexOutOfRangeException"></exception>
        public static List<Cell3D<T>> FromStartToEndInDesiredDirectionToList<T>(this T[,,] array, Index3D startIndex, Index3D direction) where T : IComparable<T>
        {
            if (!array.IsInBoundary(startIndex))
            {
                string message = string.Format("The given index is not in the boundaries of the array! Given index:{0} , Max index in the array:{1}", startIndex, new Index3D(array.GetLength(0), array.GetLength(1), array.GetLength(2)));
                throw new IndexOutOfRangeException(message);
            }

            List<Cell3D<T>> elements = new List<Cell3D<T>>();
            Index3D currentIndex = startIndex;

            while (array.IsInBoundary(currentIndex))
            {
                Cell3D<T> element = new Cell3D<T>(array.GetValue(currentIndex), currentIndex);
                elements.Add(element);

                currentIndex += direction;
            }

            return elements;
        }

        /// <summary>
        /// Gets the straight neighbours. Top/Down , Left/Right and 'In front of' and behind the given cell
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="startIndex">The start index.</param>
        /// <exception cref="System.IndexOutOfRangeException"></exception>
        public static List<Cell3D<T>> GetStraightNeighbours<T>(this T[,,] array, Index3D startIndex) where T : IComparable<T>
        {
            if (!array.IsInBoundary(startIndex))
            {
                string message = string.Format("The given index is not in the boundaries of the array! Given index:{0} , Max index in the array:{1}", startIndex, new Index3D(array.GetLength(0), array.GetLength(1), array.GetLength(2)));
                throw new IndexOutOfRangeException(message);
            }

            List<Cell3D<T>> neighbours = new List<Cell3D<T>>();
            Index3D currentIndex;

            foreach (Index3D direction in Index3D.StraightDirections)
            {
                currentIndex = startIndex + direction;

                if (array.IsInBoundary(currentIndex))
                    neighbours.Add(new Cell3D<T>(array.GetValue(currentIndex), currentIndex));
            }

            return neighbours;
        }

        /// <summary>
        /// Gets all the neighbours of the given cell
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="startIndex">The start index.</param>
        /// <exception cref="System.IndexOutOfRangeException"></exception>
        public static List<Cell3D<T>> GetStraightAndDiagonalNeighbours<T>(this T[,,] array, Index3D startIndex) where T : IComparable<T>
        {
            if (!array.IsInBoundary(startIndex))
            {
                string message = string.Format("The given index is not in the boundaries of the array! Given index:{0} , Max index in the array:{1}", startIndex, new Index3D(array.GetLength(0), array.GetLength(1), array.GetLength(2)));
                throw new IndexOutOfRangeException(message);
            }

            List<Cell3D<T>> neighbours = new List<Cell3D<T>>();
            Index3D currentIndex;

            foreach (Index3D direction in Index3D.StraightDirections)
            {
                currentIndex = startIndex + direction;

                if (array.IsInBoundary(currentIndex))
                    neighbours.Add(new Cell3D<T>(array.GetValue(currentIndex), currentIndex));
            }

            return neighbours;
        }
    }
}
